/*
 * Copyright (C) 2015, apexes.net. All rights reserved.
 * 
 *        http://www.apexes.net
 * 
 */
package net.apexes.wsonrpc.client.support;

import net.apexes.wsonrpc.client.WebsocketConnector;
import net.apexes.wsonrpc.client.WsonrpcClientEndpoint;
import net.apexes.wsonrpc.client.support.websocket.WebSocketClient;
import net.apexes.wsonrpc.client.support.websocket.WebSocketEventHandler;
import net.apexes.wsonrpc.client.support.websocket.WebSocketException;
import net.apexes.wsonrpc.client.support.websocket.WebSocketMessage;
import net.apexes.wsonrpc.core.WebSocketSession;

import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
import java.io.IOException;
import java.net.URI;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * 基于 {@link WebSocketClient}的连接
 *
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 *
 */
public class SimpleWebsocketConnector implements WebsocketConnector {

    private final int connectTimeout;
    private final int soTimeout;
    private SocketFactory socketFactory;
    private HostnameVerifier hostNameVerifier;

    public SimpleWebsocketConnector() {
        this(5000, 0);
    }

    public SimpleWebsocketConnector(int connectTimeout, int soTimeout) {
        this.connectTimeout = connectTimeout;
        this.soTimeout = soTimeout;
    }

    public void setSocketFactory(SocketFactory socketFactory) {
        this.socketFactory = socketFactory;
    }

    public void setHostnameVerifier(HostnameVerifier hostNameVerifier) {
        this.hostNameVerifier = hostNameVerifier;
    }

    @Override
    public void connectToServer(final WsonrpcClientEndpoint endpoint, URI uri) throws Exception {
        WebSocketClient wsClient = new WebSocketClient(uri) {
            @Override
            protected void onPong(byte[] payload) {
                endpoint.onPong(payload);
            }
        };
        wsClient.setSocketFactory(socketFactory);
        wsClient.setHostnameVerifier(hostNameVerifier);
        WebSocketClientProxy proxy = new WebSocketClientProxy(endpoint, wsClient, connectTimeout, soTimeout);
        wsClient.setEventHandler(proxy);
        proxy.connectToServer();
    }
    
    /**
     * 
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     *
     */
    private static class WebSocketClientProxy implements WebSocketSession, WebSocketEventHandler {

        private final Object syncConnecting = new Object();
        private final CountDownLatch connectLatch;
        private final WsonrpcClientEndpoint endpoint;
        private final WebSocketClient wsClient;
        private final int connectTimeout;
        private final int soTimeout;
        private WebSocketException connectError;
        private String id;
        private volatile boolean opened;
        
        WebSocketClientProxy(WsonrpcClientEndpoint endpoint, WebSocketClient wsClient, int connectTimeout, int soTimeout) {
            this.endpoint = endpoint;
            this.wsClient = wsClient;
            this.connectTimeout = connectTimeout;
            this.soTimeout = soTimeout;
            this.connectLatch = new CountDownLatch(1);
            this.opened = false;
        }

        void connectToServer() throws Exception {
            connectError = null;
            wsClient.connect(connectTimeout, soTimeout);

            if (connectTimeout > 0) {
                if (!connectLatch.await(connectTimeout, TimeUnit.MILLISECONDS) && connectError == null) {
                    connectError = new WebSocketException("connect timeout.");
                }
            } else {
                connectLatch.await();
            }
            if (connectError != null) {
                throw connectError;
            }
            synchronized (syncConnecting) {
                if (wsClient.getState() == WebSocketClient.State.CONNECTING && !opened) {
                    wsClient.close();
                    throw new TimeoutException("connect server timeout.");
                }
            }
        }
    
        @Override
        public void onOpen() {
            synchronized (syncConnecting) {
                if (wsClient.getState() == WebSocketClient.State.CONNECTED) {
                    id = UUID.randomUUID().toString();
                    opened = true;
                    connectLatch.countDown();
                    endpoint.onOpen(this);
                }
            }
        }
    
        @Override
        public void onClose() {
            if (opened) {
                opened = false;
                wsClient.close();
                endpoint.onClose(0, "");
            }
        }
    
        @Override
        public void onMessage(WebSocketMessage message) {
            endpoint.onMessage(message.getBytes());
        }
    
        @Override
        public void onError(WebSocketException error) {
            switch (wsClient.getState()) {
                case NONE:
                case CONNECTING:
                    connectError = error;
                    connectLatch.countDown();
                    break;
                default:
                    endpoint.onError(error);
                    break;
            }
        }
    
        @Override
        public String getId() {
            return id;
        }

        @Override
        public boolean isOpen() {
            return opened;
        }

        @Override
        public void sendBinary(byte[] bytes) throws IOException {
            wsClient.send(bytes);
        }

        @Override
        public void ping(byte[] bytes) throws IOException {
            wsClient.ping(bytes);
        }

        @Override
        public void close() throws IOException {
            wsClient.close();
        }
    }
}
